package fr.inserm.u915.tabixserver;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLStreamException;



import net.sf.samtools.tabix.TabixReader;



@SuppressWarnings("serial")
public class AnnotationServlet extends HttpServlet
	{
	private abstract class AbstractTextHandler
	extends Handler
		{
		protected PrintStream out;
		AbstractTextHandler(OutputStream out,String encoding) throws IOException
			{
			this.out=new PrintStream(out, false, encoding);
			}
		@Override
		public void writeEndDocument() throws IOException {
			this.out.flush();
			}
		}
	
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/

	private class PlainTextHandler
		extends AbstractTextHandler
		{
		private int n_printed=0;
		PlainTextHandler(OutputStream out,String encoding) throws IOException
			{
			super(out,encoding);
			}
		@Override
		public void writeStartDocument() throws IOException
			{
			}
		@Override
		public void writeStartQuery(Segment segment) throws IOException {
			super.writeStartQuery(segment);
			if(n_printed>0) this.out.println("\\\\");
			n_printed++;
			out.println("##instance.id="+getInstance().getId());
			out.println("##instance.label="+getInstance().getLabel());
			out.println("##instance.description="+getInstance().getDescription());
			out.println("##region="+segment);
			boolean found=false;
		    for(int i=0;i<getInstance().size();++i)
		    		{
		    		Column col= getInstance().get(i);
		    		if(col.isIgnore()) continue;
		    		if(found) out.print("\t");
		    		found=true;
					out.print(col.getLabel());
		    		}
		   out.println();
			}
		
		@Override
		public int writeRow(String[] tokens) throws IOException
			{
			boolean found=false;
		    for(int i=0;i<getInstance().size() && i<tokens.length;++i)
		    		{
		    		Column col= getInstance().get(i);
		    		if(col.isIgnore()) continue;
		    		if(found) out.print("\t");
		    		found=true;
					out.print(tokens[i]);
		    		}
		    out.println();
			return _next();
			}
		}
	
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/

	private class PlainTextCountHandler
		extends AbstractTextHandler
		{
		PlainTextCountHandler(OutputStream out,String encoding) throws IOException
			{
			super(out,encoding);
			}
		@Override
		public void writeStartDocument() throws IOException
			{
			out.println("#instance\tsegment\tcount");
			}
		@Override
		public void writeStartQuery(Segment segment) throws IOException {
			super.writeStartQuery(segment);
			out.print(getInstance().getId());
			out.print("\t");
			out.print(segment.toString());
			out.print("\t");
			}
		@Override
		public void writeEndQuery() throws IOException {
			out.println(super.countRows);
			super.writeEndQuery();
			}
		@Override
		public int writeRow(String[] tokens) throws IOException
			{
			return _next();
			}
		}

	
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/
	
	private class JSONHandler extends AbstractTextHandler
		{
		int n_instances=0;
		int n_segments=0;
		public JSONHandler(OutputStream os,String encoding) throws IOException
			{
			super(os,encoding);
			}
		@Override
		public void writeStartDocument() throws IOException
			{
			this.out.print("[");
			}
	
		@Override
		public void writeEndDocument() throws IOException
			{
			this.out.println("]");
			super.writeEndDocument();
			}
	
		@Override
		public void writeStartInstance(Instance instance) throws IOException
			{
			super.writeStartInstance(instance);
			if(n_instances>0) this.out.print(",");
			n_instances++;
			n_segments=0;
			this.out.print("{\"id\":");
			this.out.print(quote(instance.getId()));
			this.out.print(",\"label\":");
			this.out.print(quote(instance.getLabel()));
			this.out.print(",\"description\":");
			this.out.print(quote(instance.getDescription()));
			this.out.print(",\"segments\"");
			this.out.print(":[");
			}
		
		@Override
		public void writeEndInstance() throws IOException
			{
			this.out.print("]}");
			super.writeEndInstance();
			}
		

		
		@Override
		public void writeStartQuery(Segment segment) throws IOException
			{
			super.writeStartQuery(segment);
			if(n_segments>0) this.out.print(",");
			n_segments++;
			this.out.print("{\"build\":");
			this.out.print(quote(getInstance().getBuild().getId()));
			this.out.print(",\"chrom\":");
			this.out.print(quote(segment.getChromosome()));
			this.out.print(",\"start\":");
			this.out.print(segment.getStart());
			this.out.print(",\"end\":");
			this.out.print(segment.getEnd());
			_printStartRows();
			}
		
		protected void _printStartRows() throws IOException
			{
			this.out.print(",\"rows\":[");
			}
	
		
		protected void _printEndRows() throws IOException
			{
			this.out.print("]}");
			}

		
		@Override
		public void writeEndQuery() throws IOException
			{
			_printEndRows();
			super.writeEndQuery();
			}
	
		
		@Override
		public int writeRow(String[] tokens) throws IOException
			{
			if(this.countRows>0) this.out.print(",");
			this.out.print("{");
			boolean found=false;
		    for(int i=0;i<getInstance().size() && i<tokens.length;++i)
		    		{
		    		Column col= getInstance().get(i);
		    		if(col.isIgnore()) continue;
		    		if(found) out.print(",");
					found=true;
		    		out.print(quote(col.getId()));
					out.print(":");
					String quoted=quote(tokens[i]);
					if(tokens[i].equalsIgnoreCase("null"))
						{
						quoted="null";
						}
					switch(col.getDataType())
						{
						case BOOLEAN:
							{
							if(tokens[i].equals("true") || tokens[i].equals("false"))
								{
								quoted=tokens[i];
								}
							break;
							}
						case DOUBLE:
							{
							try
								{
								new BigDecimal(tokens[i]);
								quoted=tokens[i];
								}
							catch(NumberFormatException err)
								{
								
								}
							break;
							}
						case INT:
							{
							try
								{
								new BigInteger(tokens[i]);
								quoted=tokens[i];
								}
							catch(NumberFormatException err)
								{
								
								}
							break;
							}
						case STRING://through
						default:
							{
							break;
							}
						}
					out.print(quoted);
		    		}
			this.out.print("}");
			return _next();
			}
		
		private String quote(String s)
			{
			StringBuilder b=new StringBuilder(s.length()+1);
			b.append("\"");
			for(int i=0;i< s.length();++i)
				{
				switch(s.charAt(i))
					{
					case '\b':b.append("\\b"); break;
					case '\n':b.append("\\n"); break;
					case '\t':b.append("\\t"); break;
					case '\r':b.append("\\r"); break;
					case '\'':b.append("\\\'"); break;
					case '\"':b.append("\\\""); break;
					case '\\':b.append("\\\\"); break;
					default:b.append(s.charAt(i));break;
					}
				}
			b.append("\"");
			return b.toString();
			}
		}
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/
	
	private class JSONCountHandler extends JSONHandler
		{
		public JSONCountHandler(OutputStream os,String encoding) throws IOException
			{
			super(os,encoding);
			}
		
		@Override
		protected void _printStartRows() throws IOException
			{
			this.out.print(",\"rows\":");
			}
		@Override
		protected void _printEndRows() throws IOException
			{
			this.out.print(getRowCount());
			this.out.print("}");
			}
		
		@Override
		public int writeRow(String[] tokens) throws IOException
			{
			return _next();
			}
		}
	
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/
	
	
	public class HtmlCountHandler
	extends AbstractXmlHandler
		{
		public HtmlCountHandler(OutputStream out, String encoding)
				throws IOException
			{
			super(out, encoding);
			}
		@Override
		public void writeStartDocument() throws IOException
			{
			//dont write XML header
			try
				{
				getWriter().writeStartElement("html");
				getWriter().writeStartElement("head");
				getWriter().writeEndElement();//head
				getWriter().writeStartElement("body");
				getWriter().writeStartElement("table");
				
				
				getWriter().writeStartElement("thead");
				getWriter().writeStartElement("tr");
				
				for(String head:new String[]{"Table","Build","Chrom","Start","End","Count"})
					{
					getWriter().writeStartElement("th");
					getWriter().writeCharacters(head);
					getWriter().writeEndElement();
					}
				
				
				getWriter().writeEndElement();//tr
				getWriter().writeEndElement();//thead
	
				getWriter().writeStartElement("tbody");//tbody
				}
			catch (XMLStreamException err)
				{
				throw new IOException(err);
				}
			}
		
		@Override
		public void writeEndDocument() throws IOException
			{
			try
				{
				getWriter().writeEndElement();//tbody
				getWriter().writeEndElement();//table
				getWriter().writeEndElement();//body
				getWriter().writeEndElement();//html
				getWriter().flush();
				getWriter().close();
				}
			catch (XMLStreamException err)
				{
				throw new IOException(err);
				}
			}
			
		@Override
		public void writeEndQuery() throws IOException
			{
			try
				{
				getWriter().writeStartElement("tr");
				for(String td:new String[]{
						getInstance().getLabel(),
						getInstance().getBuild().getId(),
						getSegment().getChromosome(),
						String.valueOf(getSegment().getStart()),
						String.valueOf(getSegment().getEnd()),
						String.valueOf(getRowCount())
						})
					{
					getWriter().writeStartElement("td");
					getWriter().writeCharacters(td);
					getWriter().writeEndElement();
					}
				getWriter().writeEndElement();
				}
			catch (XMLStreamException err)
				{
				throw new IOException(err);
				}
			super.writeEndQuery();
			}

		@Override
		public int writeRow(String[] tokens) throws IOException
			{
			return _next();
			}
		
		}
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/
	private static class XmlCountHandler extends XmlHandler
		{
		public XmlCountHandler(OutputStream out,String encoding) throws IOException
			{
			super(out,encoding);
			}
		@Override
		public void writeEndQuery() throws IOException
			{
			try
				{
				getWriter().writeAttribute("count", String.valueOf(getRowCount()));
				}
			catch (XMLStreamException e)
				{
				throw new IOException(e);
				}
			super.writeEndQuery();
			}
		
		@Override
		public int writeRow(String[] tokens) throws IOException
			{
			return _next();
			}

		}
	/*************************************************************************************/
	/*************************************************************************************/
	/*************************************************************************************/
	


	
	@SuppressWarnings("unchecked")
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException
		{
		final Model model=(Model)this.getServletContext().getAttribute(TabixCtxListener.TABIX_MODEL_ATTRIBUTE);
		if(model==null) throw new ServletException("model?");
	
		String query=req.getParameter("q");
		if(query==null) throw new ServletException("no query");
		
		Build build=(Build)req.getAttribute("build");
		if(build==null) throw new ServletException("build?");
		
		File fastaFile=build.getFastaFile();
		if(fastaFile==null)
			{
			throw new ServletException("No fasta file defined for build id "+build);
			}
		
		List<Segment> segs=(List<Segment>)req.getAttribute("segments");
		if(segs==null) throw new ServletException("segs?");

		Set<String> selectedInstance=new HashSet<String>();
		String instances[] =req.getParameterValues("t");
		if(instances!=null)
			{
			for(String id:instances)
				{
				selectedInstance.add(id);
				}
			}
		boolean count=req.getParameter("count")!=null;
		String format=req.getParameter("format");
		if(format==null) format="text";
		
		if(format.equals("svg"))
			{
			req.getRequestDispatcher("/WEB-INF/svg").forward(req, resp);
			return;
			}
		
		String filename=(segs.size()==1?segs.get(0).toString():String.valueOf(segs.size())+" segments");
		filename=filename.replaceAll("[ ,\\:\\+\t]+", "_");
		
		
		
		
		OutputStream respout= resp.getOutputStream();
		OutputStream out=respout;
		GZIPOutputStream gzout=null;
		boolean forceSaveAs=false;
		String compress=req.getParameter("gzip");
		if(compress!=null && compress.equals("yes"))
			{
			gzout=new GZIPOutputStream(out);
			out=gzout;
			forceSaveAs=true;
			}
	
		String contentType=null;
		Handler handler=null;
		if(format.equalsIgnoreCase("html"))
			{
			contentType="text/html";
			filename+=".html";
			handler=(count?
					new HtmlCountHandler(out, resp.getCharacterEncoding()) :
					new HtmlHandler(out, resp.getCharacterEncoding())
					)
					;
			}
		else if(format.equalsIgnoreCase("xml"))
			{
			contentType="text/xml";
			filename+=".xml";
			
			String baseuri=req.getScheme()+"://"+
					req.getServerName()+":"+
					req.getServerPort()
					+req.getServletContext().getContextPath()+"/tabix/build/"+
					build.getId()
					;
			
			String xsduri=baseuri+".xsd";
		
			if(count)
				{
				XmlCountHandler h2=new XmlCountHandler(out, resp.getCharacterEncoding());
				h2.xsduri=xsduri;
				handler=h2;
				}
			else
				{
				String xsluri = baseuri+".xsl";
				XmlHandler h2=new XmlHandler(out, resp.getCharacterEncoding());
				h2.setStylesheetURI(xsluri);
				h2.xsduri=xsduri;
				handler=h2;
				}
			}
		else if(format.equalsIgnoreCase("json"))
			{
			contentType="application/json";
			filename+=".json";
			handler=(count?
					new JSONCountHandler(out, resp.getCharacterEncoding()):
					new JSONHandler(out,resp.getCharacterEncoding())
					);
			}
		else if(format.equalsIgnoreCase("text") || format.equalsIgnoreCase("txt"))
			{
			filename+=".txt";
			contentType="text/plain";
			handler=(count?
					  new PlainTextCountHandler(out, resp.getCharacterEncoding())
					: new PlainTextHandler(out, resp.getCharacterEncoding())
					);
			}
		else
			{
			throw new ServletException("unknown format : "+format);
			}
		
		if(gzout!=null)
			{
			contentType="application/x-gzip";
			filename+=".gz";
			forceSaveAs=true;
			}
		
		resp.setHeader("Content-Disposition",
				(forceSaveAs?"attachment":"inline")+
				"; filename="+filename
				);
		
		resp.setContentType(contentType);
		
		handler.writeStartDocument();
		
		for(Instance instance:build.getAllInstances())
			{
			if(!selectedInstance.contains(instance.getId())) continue;
			if(instance.getTable()==null) throw new NullPointerException(instance.toString());
			Pattern regex=Pattern.compile(instance.getTable().getPattern());
			handler.writeStartInstance(instance);
			TabixReader r=new TabixReader(instance.getPath());
			for(Segment segment:segs)
				{
				handler.writeStartQuery(segment);
				Segment normalizeSeg=handler.getSegment().cast(instance.getTable().getChromosomeNaming());
				Iterator<String> iter=r.query(
						normalizeSeg.getChromosome(),
						normalizeSeg.getStart(),
						normalizeSeg.getEnd()
						);
				while(iter.hasNext())
					{
					String line=iter.next();
					String tokens[]=null;
					if(!count)
						{
						tokens=regex.split(line);
						if(tokens.length<instance.size())
							{
							continue;
							}
						}
					int ret=handler.writeRow(tokens);
					if(ret==Handler.BREAK) break;
					}
				handler.writeEndQuery();
				}
			r.close();
			handler.writeEndInstance();
			}
		handler.writeEndDocument();
		
		if(gzout!=null)
			{
			gzout.finish();
			gzout.flush();
			gzout.close();
			}
		
		respout.flush();
		respout.close();
		}
	
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException
		{
		doGet(req, resp);
		}

	
	}
